/**
 * 
 */
package com.redhat.auditor.webdiff;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.Map;

import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

import org.outerj.daisy.diff.HtmlCleaner;
import org.outerj.daisy.diff.XslFilter;
import org.outerj.daisy.diff.html.HTMLDiffer;
import org.outerj.daisy.diff.html.HtmlSaxDiffOutput;
import org.outerj.daisy.diff.html.TextNodeComparator;
import org.outerj.daisy.diff.html.dom.DomTreeBuilder;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 * Implementation class for WebDiff operations.
 * <p>
 * This implementation class should not be allowed to be instantiated by other
 * classes or objects. It is purely intended to provide a loosely-coupled
 * implementation detail for the functionality specified in the interfacing
 * class {@link WebDiff}. The underlying implementation should never be directly
 * accessed by users, and the operations implemented here should be accessed via
 * the interfacing class in the manner described in the WebDiff public API.
 * 
 * @since WebDiff 0.1
 * @version %I%, %G%
 * @author Trungtin Ton
 * 
 */
public class WebDiffImpl extends WebDiff {

	/**
	 * @note the current WebDiff Implementation utilizes the DaisyDiff Java
	 *       libraries for its HTML-diffing functionality.
	 */

	/**
	 * The default locale used in this class for locale sensitive operations
	 * where a locale is not specified, or specifying the locale is not
	 * supported.
	 */
	private static final Locale LOCALE = Locale.getDefault();

	/**
	 * Convenience constant for the lower-case string literal "diffreport".
	 */
	private static final String DIFFREPORT = "diffreport";
	/**
	 * Convenience constant for the lower-case string literal "diff".
	 */
	private static final String DIFF = "diff";
	/**
	 * Convenience constant for the empty string literal ("").
	 */
	private static String EMPTY = "";

	/**
	 * Default, protected, null constructor.
	 */
	protected WebDiffImpl() {

	}

	/**
	 * Executes a WebDiff compare operation on the given source and destination
	 * InputStreams (assuming that the provided input streams point to HTML
	 * documents).
	 * 
	 * @param srcDoc
	 *            the reference (for example a URL or local file-path) for the
	 *            source (or old) HTML document.
	 * @param destDoc
	 *            the reference (for example a URL or local file-path) for the
	 *            destination (or new) HTML document.
	 * @param writer
	 *            the writer to which the comparison result will be appended to.
	 * @param options
	 *            a map of key-value pairs containing any additional optional
	 *            arguments that may be useful to the comparison operation. Each
	 *            of the arguments should be stored as a value and mapped to a
	 *            parameter name that is recognized by the operation according
	 *            to the WebDiff public API.
	 * @throws TransformerConfigurationException
	 *             when a transformer configuration error occurs.
	 * @throws TransformerFactoryConfigurationError
	 *             when a transformer factory configuration error occurs.
	 * @throws IOException
	 *             when an IO error occurs.
	 * @throws SAXException
	 *             when an XML parsing error occurs.
	 * @throws URISyntaxException
	 *             when a URI syntax error occurs.
	 */
	@Override
	protected void doCompare(final String srcDoc, final String destDoc,
			final Writer writer, final Map<String, Object> options)
			throws TransformerConfigurationException,
			TransformerFactoryConfigurationError, IOException, SAXException,
			URISyntaxException {

		// Construct Left and Right Comparator assets from input streams
		final HtmlCleaner cleaner = new HtmlCleaner();

		// SOURCE DOCUMENT:
		final DomTreeBuilder l = new DomTreeBuilder();
		cleaner.cleanAndParse(new InputSource(getInputStream(srcDoc)), l);
		final TextNodeComparator left = new TextNodeComparator(l, LOCALE);

		// DESTINATION DOCUMENT:
		final DomTreeBuilder r = new DomTreeBuilder();
		cleaner.cleanAndParse(new InputSource(getInputStream(destDoc)), r);
		final TextNodeComparator right = new TextNodeComparator(r, LOCALE);

		// Create a new empty result container used to populate the writer.
		final TransformerHandler result = ((SAXTransformerFactory) TransformerFactory
				.newInstance()).newTransformerHandler();
		result.setResult(new StreamResult(writer));

		final ContentHandler p = new XslFilter().xsl(result,
				"org/outerj/daisy/diff/htmlheader.xsl");

		// Process with DaisyDiff and write result to the given content handler.
		p.startDocument();
		p.startElement(EMPTY, DIFFREPORT, DIFFREPORT, new AttributesImpl());
		appendCSSLinksToContentHandler((String[]) options.get("css"), p);
		p.startElement(EMPTY, DIFF, DIFF, new AttributesImpl());

		new HTMLDiffer(new HtmlSaxDiffOutput(p, DIFF)).diff(left, right);
		p.endElement(EMPTY, DIFF, DIFF);
		p.endElement(EMPTY, DIFFREPORT, DIFFREPORT);
		p.endDocument();
	}

	/**
	 * Utility method that produces an input stream from the given a reference
	 * to an external document or resource. This method processes the document
	 * reference to determine the nature of the reference, and the input stream
	 * returned will be of the corresponding type. For example, if the document
	 * reference is a path to a local file, then a FileInputStream will be
	 * returned.
	 * 
	 * @param documentRef
	 *            the reference to the document for which a stream will be
	 *            returned, for example a URI or a file path.
	 * @return a corresponding {@code InputStream} for the given reference.
	 * @throws URISyntaxException
	 *             if the reference is identified as a URI but could not be
	 *             validly parsed as a URI reference.
	 * @throws IOException
	 *             if the reference is identified as a local file but a
	 *             {@code FileInputStream} failed to be created due to a failed
	 *             or interrupted I/O operation.
	 * @throws MalformedURLException
	 *             if for some reason the reference is identified as a URI but
	 *             is either not a valid URL or is a malformed URL. This may be
	 *             because no legal protocol could be found or the string could
	 *             not be parsed.
	 */
	private static InputStream getInputStream(final String documentRef)
			throws MalformedURLException, IOException, URISyntaxException {

		if (documentRef.startsWith("http://"))
			return new URI(documentRef).toURL().openStream();
		else
			return new FileInputStream(documentRef);
	}

	/**
	 * Processes and appends each of the specified CSS links to the specified
	 * content handler.
	 * 
	 * @param css
	 *            the array of external CSS links to be attached to the
	 *            specified content handler.
	 * @param handler
	 *            the handler to which the specified external CSS links will be
	 *            attached.
	 * @throws SAXException
	 *             if an XML parsing error occurs.
	 */
	private static void appendCSSLinksToContentHandler(final String[] css,
			final ContentHandler handler) throws SAXException {
		if (css != null) {
			handler.startElement(EMPTY, "css", "css", new AttributesImpl());
			for (final String cssLink : css) {
				final AttributesImpl attr = new AttributesImpl();
				attr.addAttribute(EMPTY, "href", "href", "CDATA", cssLink);
				attr.addAttribute(EMPTY, "type", "type", "CDATA", "text/css");
				attr.addAttribute(EMPTY, "rel", "rel", "CDATA", "stylesheet");
				handler.startElement(EMPTY, "link", "link", attr);
				handler.endElement(EMPTY, "link", "link");
			}
			handler.endElement(EMPTY, "css", "css");
		}
	}

	@Override
	protected void doCompare(InputStream srcDocument, InputStream destDocument,
			Writer writer, Map<String, Object> options) throws Exception {

		// Construct Left and Right Comparator assets from input streams
		final HtmlCleaner cleaner = new HtmlCleaner();

		// SOURCE DOCUMENT:
		final DomTreeBuilder l = new DomTreeBuilder();
		cleaner.cleanAndParse(new InputSource(srcDocument), l);
		final TextNodeComparator left = new TextNodeComparator(l, LOCALE);

		// DESTINATION DOCUMENT:
		final DomTreeBuilder r = new DomTreeBuilder();
		cleaner.cleanAndParse(new InputSource(destDocument), r);
		final TextNodeComparator right = new TextNodeComparator(r, LOCALE);

		// Create a new empty result container used to populate the writer.
		final TransformerHandler result = ((SAXTransformerFactory) TransformerFactory
				.newInstance()).newTransformerHandler();
		result.setResult(new StreamResult(writer));

		final ContentHandler p = new XslFilter().xsl(result,
				"org/outerj/daisy/diff/htmlheader.xsl");

		// Process with DaisyDiff and write result to the given content handler.
		p.startDocument();
		p.startElement(EMPTY, DIFFREPORT, DIFFREPORT, new AttributesImpl());
		if (options != null) {
			appendCSSLinksToContentHandler((String[]) options.get("css"), p);
		}
		p.startElement(EMPTY, DIFF, DIFF, new AttributesImpl());

		new HTMLDiffer(new HtmlSaxDiffOutput(p, DIFF)).diff(left, right);
		p.endElement(EMPTY, DIFF, DIFF);
		p.endElement(EMPTY, DIFFREPORT, DIFFREPORT);
		p.endDocument();
	}

}
